package archive.zip;

import archive.Archiver;
import com.jfoenix.controls.JFXProgressBar;
import org.junit.jupiter.api.Test;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Timer;
import java.util.TimerTask;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

/**
 * zip压缩的核心类。具备多文件/文件夹压缩、解压的功能
 * 作者：佘一凡
 * 时间：2019-12-29
 */

public class Ziper implements Archiver {
    private long srcSize = 0;
    private long archivedSize = 0;
    private final int MAX_CAPACITY = 1024;
    private volatile boolean adminArchive = false;

    private long zipSrcSize = 0;
    private long zipUnarchivedSize = 0;
    private volatile boolean adminUnarchive = false;

    // 传入进度条对象监控压缩和解压进度
    JFXProgressBar archiveProgress;
    JFXProgressBar unarchiveProgress;

    /**
     * Constructor
     */
    public Ziper(JFXProgressBar archiveProgress, JFXProgressBar unarchiveProgress) {
        this.archiveProgress = archiveProgress;
        this.unarchiveProgress = unarchiveProgress;
    }

    @Test
    void sizeTest() {
        String[] srcs = {
                "/Users/sakai/Desktop/Github",
                "/Users/sakai/Desktop/documents",
                "/Users/sakai/Desktop/test.txt"
        };
        setSrcSize(srcs);
        System.out.println(this.srcSize / 1024.0 + "KB; " + this.srcSize / 1024.0 / 1024.0 + "MB; " + this.srcSize / 1024.0 / 1024.0 / 1024.0);
    }
    @Test
    void zipSizeTest() {
        String src = "/Users/sakai/Desktop/jar/jetbrains-agent.jar";
        setZipSrcSize(src);
    }
    @Test
    void zipTest() {
        String[] srcs = {
                "/Users/sakai/Desktop/GitHub",
                "/Users/sakai/Desktop/documents",
                "/Users/sakai/Desktop/test.txt"
        };
        archive(srcs, "/Users/sakai/Desktop/sheyifan/test.zip", "", 6);
    }


    /**
     * @param dir：
     *           directory of current recursion.
     * 功能：计算文件夹的总大小
     */
    private void walkForSize(File dir) {
        File[] files = dir.listFiles();
        for(File f: files) {
            if(f.isFile()) {
                this.srcSize += f.length();
            }
            else {
                walkForSize(f);
            }
        }
    }
    private void setSrcSize(String[] srcs) {
        for(String src: srcs) {
            File srcFile = new File(src);
            if(srcFile.isFile()) {
                srcSize += srcFile.length();
            }
            else {
                walkForSize(srcFile);
            }
        }
    }
    private void setZipSrcSize(String zipSrc) {
        try(ZipInputStream zis = new ZipInputStream(new DataInputStream(new BufferedInputStream(new FileInputStream(zipSrc))), StandardCharsets.UTF_8)) {
            ZipFile srcZip = new ZipFile(zipSrc);

            Enumeration<? extends ZipEntry> zipSizeEnumForBug = srcZip.entries();

            while(zipSizeEnumForBug.hasMoreElements()) {
                // 用来获取entry真正的大小
                ZipEntry currentEntrySizeForBug = zipSizeEnumForBug.nextElement();
                zipSrcSize += currentEntrySizeForBug.getSize();
                System.out.println("当前Entry:" + currentEntrySizeForBug +"; 大小：" + currentEntrySizeForBug.getSize());
            }
            System.out.println("总大小：" + zipSrcSize);
        }catch(IOException ioe) {
            ioe.printStackTrace();
        }
    }


    /**
     *
     * @param srcs:
     *            Source files waiting for zipping, regardless of directories or files.
     * @param des:
     *           Destination directory to store unzipped files or directories.
     * @param comment:
     *               comment for zip files or entries within it.
     * @param level:
     *             compressing level, higher level will make smaller size of destination zip file and slower compressing/uncompressing rate.
     */
    @Override
    public void archive(String[] srcs, String des, String comment, int level) {

        // 首先要设置好所有原文件的大小，用作计算压缩进度
        setSrcSize(srcs);

        // 设置标志位，代表开始监控压缩进度
        this.adminArchive = true;

        // 创建目标压缩文件（如果该文件不存在）
        Path desPath = Paths.get(des);
        Path desParent = desPath.getParent();
        if(Files.notExists(desPath)) {
            try {
                // 如果压缩文件的父级目录不存在，则弹出提示，经允许之后创建父级目录
                if(Files.notExists(desParent)) {
                    // 弹出提示框。经过允许之后再创建文件夹
                    desDirNotFound(desParent);
                    Files.createFile(desParent);
                }
                Files.createFile(desPath);
            }catch(IOException ioe) {
                ioe.printStackTrace();
            }
        }
        else {
            // 目标压缩文件已经存在。此时弹出"文件已存在"的提示框，并禁止继续压缩文件
            desZipAlreadyExists(desPath);
        }

        // 新开一个线程来监控压缩的进度。监控的时间间隔为10毫秒
        new Thread(() -> new Timer().scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                if(!adminArchive) {
                    // 接收到结束标志的时候停止定时器
                    this.cancel();
                }
                archiveProgress.setProgress(archivedSize/(double)srcSize);
            }
        }, 0, 10), "archive_progress").start();

        try(ZipOutputStream zos = new ZipOutputStream(new DataOutputStream(new BufferedOutputStream(new FileOutputStream(des))))) {

            // 设置压缩文件的注释
            zos.setComment(comment);
            // 设置压缩等级
            zos.setLevel(level);


            for(String src: srcs) {
                Path srcPath = Paths.get(src);

                // 检查文件/文件夹是否存在
                if(Files.notExists(srcPath)) {
                    srcNotFound(src);
                    // 找不到原文件，此时立即停止压缩，删除已经生成的压缩文件，随之弹出错误提示。
                    Files.delete(desPath);
                    // 让程序立即停止压缩
                    throw new FileNotFoundException();
                }

                // 如果被压缩的对象是文件
                if(!Files.isDirectory(srcPath)) {
                    DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(src)));
                    ZipEntry nextEntry = new ZipEntry(srcPath.getFileName().toString());
                    zos.putNextEntry(nextEntry);

                    // 每次读取1k的数据
                    byte[] readBytes = new byte[MAX_CAPACITY];
                    // 从输入流读取数据，然后写入输出流
                    readSrcsToWrite(dis, readBytes, zos);

                    // 关闭输入流，以及当前正在使用的ZipEntry
                    zos.closeEntry();
                    dis.close();
                }
                else {
                    // 根路径为原文件/文件夹路径的父级路径
                    String root = Paths.get(src).getParent().toString();
                    // 在不同层级的文件夹中进行递归
                    walkDir(srcPath, zos, srcPath.getFileName().toString(), root);
                }
            }
        }catch(IOException ioe) {
            ioe.printStackTrace();
        }
        accomplishArchive(des);

        // 翻转标志位
        this.adminArchive = false;
    }



    /**
     * @param src:
     *           Absolute path of zip file
     * @param des:
     *           Destination path of unzipping
     */
    @Override
    public void unArchive(String src, String des) {

        File srcFile = new File(src);
        File desFile = new File(des);
        if(!srcFile.exists()) {
            // 如果原zip文件不存在，则跳出"文件不存在"的提示框
            srcNotFound(src);
        }
        if(!desFile.exists()) {
            // 如果解压的目标路径不存在，则弹出"目标路径不存在"的提示框。经过允许之后再创建文件夹
            desDirNotFound(Paths.get(des));
        }

        setZipSrcSize(src);
        this.adminUnarchive = true;

        // 新开一个线程来监控压缩的进度。监控的时间间隔为10毫秒
        new Thread(() -> new Timer().scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                unarchiveProgress.setProgress(zipUnarchivedSize/(double)zipSrcSize);
                if(!adminUnarchive) {
                    // 接收到结束标志的时候停止定时器
                    this.cancel();
                }
                archiveProgress.setProgress(archivedSize/(double)srcSize);
            }
        }, 0, 10), "unarchive_progress").start();

        try(ZipInputStream zis = new ZipInputStream(new DataInputStream(new BufferedInputStream(new FileInputStream(src))), StandardCharsets.UTF_8)) {
            ZipEntry currentEntry = null;
            Path desPath = Paths.get(des);

            // 这里使用额外的Zip枚举来修补java原声的bug（entry大小总是返回-1）。使用这个可以在读取entry的时候获取文件真正的大小。
            ZipFile srcZip = new ZipFile(src);
            ZipEntry currentEntrySizeForBug  = null;
            Enumeration<? extends ZipEntry> zipSizeEnumForBug = srcZip.entries();

            while((currentEntry = zis.getNextEntry()) != null) {
                // 用来获取entry真正的大小
                currentEntrySizeForBug = zipSizeEnumForBug.nextElement();
                if(currentEntry.isDirectory()) {
                    // 根据相对路径解析当前被解压文件夹的纯路径
                    Path dirPath = desPath.resolve(currentEntry.toString());
                    Files.createDirectories(dirPath);
                }
                else {
                    // 根据相对路径解析当前被解压文件的纯路径
                    Path filePath = desPath.resolve(currentEntry.toString());
                    // 先把文件的父级目录创建好，然后创建文件
                    if(Files.notExists(filePath.getParent()))
                        Files.createDirectories(filePath.getParent());
                    if(Files.notExists(filePath))
                        Files.createFile(filePath);

                    // 下面读取压缩文件，写入目标文件
                    String filePathStr = filePath.toAbsolutePath().toString();
                    DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(filePathStr)));


                    long fileSize = currentEntrySizeForBug.getSize();
                    final int MAX_CAP = 1024;
                    byte[] readBytes = new byte[1024];
                    // 根据每次读取的字节数(1k)计算出当前文件一共需要读取几次
                    int circulateTimes = (int)(fileSize/MAX_CAP);
                    // 计算最后一次读取当前文件的时候，需要读取多少字节
                    int remainingCount = (int)(fileSize - circulateTimes*MAX_CAP);


                    for(int i=0 ; i<circulateTimes ; i++) {
                        zis.read(readBytes);
                        dos.write(readBytes);
                        // 更新已经读取的数据量
                        zipUnarchivedSize += 1024;
                    }
                    zis.read(readBytes);
                    dos.write(readBytes, 0, remainingCount);
                    // 更新已经读取的数据量
                    zipUnarchivedSize += remainingCount;


                    zis.closeEntry();
                    dos.close();
                }
            }
        }catch(IOException ioe) {
            ioe.printStackTrace();
        }

        System.out.println("解压完成！");
        this.adminUnarchive = false;
        // 完成解压的时候弹出"解压成功"的提示窗口
        accomplishUnarchive(des);
    }



    /**
     * @param srcPath:
     *               原文件夹所对应的Path对象
     * @param zos:
     *           目标压缩文件的ZipOutputStream对象
     * @param startDir:
     *                递归所在的文件夹层。这个文件夹层是当前递归所在的文件夹纯路径相对于压缩文件纯路径的相对路径。因此初始为空
     * @param root:
     *            压缩文件所在的纯路径
     *
     * @throws IOException: 输入输出时的错误信息
     *
     * utility:在不同层的文件夹下递归
     */
    private void walkDir(Path srcPath, ZipOutputStream zos, String startDir, String root) throws IOException {
        Stream<Path> paths = Files.list(srcPath);
        Iterator<Path> pathIter = paths.iterator();
        while(pathIter.hasNext()) {
            Path nextPath = pathIter.next();


            // 如果是文件
            if(!Files.isDirectory(nextPath)) {
                // 建立到源文件的输入流
                // 获取下一个需要压缩的文件的文件名，提取出来用以给对应的压缩文件中的entry取同一名字
                DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(nextPath.toAbsolutePath().toString())));
                String nextFileName = nextPath.getFileName().toString();
                // 最前面需要拼接上当前递归所在的文件夹路径相对于压缩文件文件夹路径的相对路径，以作为递归核。不同的递归层，区别就是在不同的文件夹下。初始在根目录下，所以为空字符串。
                String nextEntryName = startDir + File.separator + nextFileName;
                zos.putNextEntry(new ZipEntry(nextEntryName));


                // 下面开始读取原文件
                // 每次读取1k的数据
                byte[] readBytes = new byte[MAX_CAPACITY];
                // 从输入流读取数据，写入输出流
                readSrcsToWrite(dis, readBytes, zos);


                dis.close();
            }
            // 如果是文件夹
            else {
                // 创建文件夹。方法是切割出文件夹路径相对于压缩文件路径的相对路径，从而作为创建Entry的依据。
                String nextDirAbsoluteName = nextPath.toAbsolutePath().toString();
                String nextDirRelativeName = nextDirAbsoluteName.replace(root, "");
                String nextEntryName = nextDirRelativeName + File.separator;
                zos.putNextEntry(new ZipEntry(nextEntryName.substring(1)));
                // 打印日志。输出当前正在处理的文件夹纯路径相对于root的相对路径。需要取出第一位后面的部分，因为第一位是'/'，这是第一层递归的残留物。


                // 获取文件夹纯路径相对于zip文件的相对路径。作为下一层递归时的参数startDir: String
                walkDir(nextPath, zos, nextDirRelativeName.substring(1), root);
            }
        }
    }

    /**
     *
     * @param inputStream:
     *                   data source.
     * @param readBytes:
     *                 restore read data.
     * @param outputStream:
     *                    output data
     * @throws IOException:
     *
     * 功能：从输入流读取数据，然后写入输出流
     */
    private void readSrcsToWrite(InputStream inputStream, byte[] readBytes, OutputStream outputStream) throws IOException {
        int readFlag;
        while((readFlag = inputStream.read(readBytes)) == MAX_CAPACITY) {
            outputStream.write(readBytes);
            // 已经压缩的进度需要更新
            archivedSize += 1024;
        }
        // 值为-1表示正好用MAX_CAP的整数倍字节数读完了文件。
        if(readFlag != -1) {
            outputStream.write(readBytes, 0, readFlag);
            // 已经压缩的进度需要更新
            archivedSize += readFlag;
        }
    }
    private void readZipToWrite(InputStream inputStream, byte[] readBytes, OutputStream outputStream) throws IOException {
        int zipReadFlag;
        while((zipReadFlag = inputStream.read(readBytes)) == MAX_CAPACITY) {
            outputStream.write(readBytes);
            // 已经压缩的进度需要更新
            zipUnarchivedSize += 1024;
        }
        // 值为-1表示正好用MAX_CAP的整数倍字节数读完了文件。
        if(zipReadFlag != -1) {
            outputStream.write(readBytes, 0, zipReadFlag);
            // 已经压缩的进度需要更新
            zipUnarchivedSize += zipReadFlag;
        }
    }

    /**
     * @param desPath:
     *               压缩文件的父级Path对象
     * 功能：压缩文件或者解压文件的过程中，如果压缩文件的目标路径不存在，则弹出提示框，作创建文件夹的提示
     */
    private void desDirNotFound(Path desPath) {

    }

    /**
     * @param desZip:
     *              压缩文件的Path对象
     * 功能：压缩文件的过程中，如果目标压缩文件本身已经存在，将禁止继续压缩文件
     */
    private void desZipAlreadyExists(Path desZip) {

    }

    /**
     * @param src:
     *           原文件的纯路径
     * 功能：压缩文件或者解压文件的过程中，如果原文件不存在，则跳出提示框，并在程序中停止压缩，删除已经生成的目标文件
     */
    private void srcNotFound(String src) {

    }

    /**
     * @param desZip:
     *              压缩文件的纯路径
     * 功能：完成压缩的时候弹出窗口，提示压缩成功
     */
    private void accomplishArchive(String desZip) {

    }

    /**
     * @param desDir:
     *              解压文件的目标路径
     * 功能：完成解压的时候弹出窗口，提示解压成功
     */
    private void accomplishUnarchive(String desDir) {

    }
}
